Skip to main content

IPF Provider

Provider supplying a list of data about trading instruments that can be set on the chart, along with their trading hours.

/**
* Interface for receiving instrument profile data - list of [InstrumentData]
*
* When loading dxCharts library, a list of instruments, their descriptions, etc. are taken from this interface
*
* When connecting dxCharts library, developer can implement this interface or use the default implementation [com.devexperts.dxcharts.provider.ipf.DxFeedHttpIpfProvider] and pass it to the library using [DxChartsDataProviders] data class
*
* Use [dataFlow] to get instrument profile data
*/
interface DxChartsIPFProvider {
/**
* Flow of receiving instrument profile data
*
* Instrument profile data is represented by list of [InstrumentData]
*/
val dataFlow: StateFlow<List<InstrumentData>>
val isLoading: StateFlow<Boolean>
}

Data is sent by updating the state of the dataFlow variable. It is represented as a list of com.devexperts.dxcharts.provider.domain.InstrumentData with objects, which stores all data about the selected instrument:

/**
* Data class for storing data about instruments
*
* @param type Type of instrument (f.e. STOCK, CRYPTO)
* @param symbol Symbol of instrument (f.e. GOOG, TSLA, AAPL)
* @param description Description of instrument (f.e. Alphabet Inc. - Class C Capital Stock)
* @param country Short name of a country of the instrument (f.e. US)
* @param currency Short name of a currency of the instrument (f.e. USD)
* @param priceIncrements Minimal price increments of the instrument (f.e. 0.01)
* @param tradingHours Trading hours of the instrument. (e.g., "BIST(name=BIST;tz=Asia/Istanbul;hd=TR;sd=TR;td=12345;de=+0000;0=10001800)")
*/
data class InstrumentData(
val type: String,
val symbol: String,
val description: String,
val country: String,
val currency: String,
val priceIncrements: String,
val tradingHours: String,
)

Received data about instruments is displayed in the list of instruments:

image-2024-1-29_0-40-22

Here is the default implementation of DxChartsIPFProvider:

/**
* Default implementation of [DxChartsIPFProvider] that uses HTTP requests to retrieve instrument data
* and trading hours information from the DxFeed API.
*
* This class provides a data flow of [InstrumentData] through a [StateFlow] and periodically
* updates the data by making HTTP requests to the DxFeed API. It also retrieves and merges
* trading hours information for the instruments.
*
* @property gson Gson instance for JSON parsing.
* @property _dataFlow Internal [MutableStateFlow] for emitting and collecting the instrument data.
* @property dataFlow Public [StateFlow] that exposes the instrument data to external observers.
* @property client [OkHttpClient] for making HTTP requests.
* @property request HTTP request configuration for retrieving instrument data.
* @property loaded Flag indicating whether data has been loaded successfully.
* @property _errorFlow Internal [MutableStateFlow] for sending errors.
* @property errorFlow [StateFlow] for sending errors.
* @property job Job instance for managing the coroutine responsible for checking data.
*/
class DxFeedHttpIpfProvider(
url: String = "",
token: String = "",
) : DxChartsIPFProvider, DxChartsErrorProvider<IpfProviderError> {
private val gson = Gson()
private val _dataFlow: MutableStateFlow<List<InstrumentData>> = MutableStateFlow(emptyList())
override val dataFlow: StateFlow<List<InstrumentData>> get() = _dataFlow
private val client = OkHttpClient()
private val request = Request.Builder()
.url(url)
.addHeader("Authorization", token)
.build()
@Volatile
private var loaded = false
private val _isLoading: MutableStateFlow<Boolean> = MutableStateFlow(false)
override val isLoading: StateFlow<Boolean> get() = _isLoading
private val _errorFlow: MutableStateFlow<IpfProviderError?> = MutableStateFlow(null)
override val errorFlow: StateFlow<IpfProviderError?> get() = _errorFlow
private var job: Job? = null
/**
* Initiates the process of requesting data from the DxFeed service.
*
* This method launches a coroutine in the background using [Dispatchers.IO] that periodically fetches data
* until [loaded] is true or data is received in [dataFlow].
*
* If a previous job is still active, it is cancelled before starting a new one.
* The coroutine continues fetching data in a loop, emitting the loading state via [_isLoading],
* calling [request] for data, and adding a delay of [FAIL_PAUSE_MS] between attempts.
*
* If an error occurs during the process, it is captured and emitted through [_errorFlow].
*/
fun connect() {
_errorFlow.tryEmit(null)
if (job != null) {
job?.cancel()
job = null
}
job = CoroutineScope(Dispatchers.IO).launch {
try {
while (!loaded && dataFlow.value.isEmpty()) {
_isLoading.emit(true)
request()
_isLoading.emit(false)
delay(FAIL_PAUSE_MS)
}
} catch (e: Exception) {
_errorFlow.tryEmit(IpfProviderError.UnknownError("Failed to fetch data: $e.message"))
}
}
}
/**
* Stops the coroutine responsible for data retrieval.
*
* This method cancels the running [job] if it exists, terminating the ongoing data retrieval process.
*
* If an error occurs during the cancellation process, it is captured and emitted through [_errorFlow].
*/
fun disconnect() {
try {
job?.cancel()
job = null
} catch (e: Exception) {
_errorFlow.tryEmit(IpfProviderError.UnknownError("Failed to shutdown executor service: $e.message"))
}
}
/**
* Makes an HTTP request to the DXFeed API to fetch instrument data.
* If the request is successful, it parses the data and updates the StateFlow.
* If the request fails, it logs an error message.
*/
private fun request() {
val call = client.newCall(request)
try {
call.execute().use { response ->
if (response.isSuccessful) {
val body = response.body?.string() ?: return
val instruments = body.parseData()
_dataFlow.tryEmit(instruments)
loaded = true
} else {
_errorFlow.tryEmit(IpfProviderError.HttpError("IPF request failed with status code $response.code: $response.message"))
}
}
} catch (e: IOException) {
_errorFlow.tryEmit(IpfProviderError.NetworkError("IPF request failed - Network Error: $e.message"))
}
}
/**
* Parses the raw data string and converts it into a list of [InstrumentData] objects.
*
* @return List of [InstrumentData] parsed from the raw data string.
*/
private fun String.parseData(): List<InstrumentData> {
val result = mutableListOf<InstrumentData>()
val typeMappings =
mutableMapOf<String, Map<String, Int>>()
try {
this.lines().forEach { line ->
if (line.startsWith("#")) {
val parts = line.removePrefix("#").split("::=")
if (parts.size == 2) {
val type = parts[0].trim()
val headers = parts[1].split(",")
typeMappings[type] =
headers.withIndex().associate { it.value.trim() to it.index }
}
} else if (line.isNotBlank()) {
val parts = splitWithQuotes(line)
val type = parts[0]
val mappings = typeMappings[type]
if (mappings != null) {
result.add(
InstrumentData(
type = parts[mappings["TYPE"] ?: 0],
symbol = parts[mappings["SYMBOL"] ?: 1],
description = parts.getOrElse(mappings["DESCRIPTION"] ?: -1) { "" },
country = parts.getOrElse(mappings["COUNTRY"] ?: -1) { "" },
currency = parts.getOrElse(mappings["CURRENCY"] ?: -1) { "" },
priceIncrements = parts.getOrElse(
mappings["PRICE_INCREMENTS"] ?: -1
) { "" },
tradingHours = parts.getOrElse(
mappings["TRADING_HOURS"] ?: -1
) { "" },
)
)
}
}
}
} catch (e: Exception) {
_errorFlow.tryEmit(
IpfProviderError.ParsingError(
"Parsing data failed: $e.message",
e
)
)
}
return result
}
/**
* Splits a string by commas, ignoring commas inside quotes. If there are empty elements between commas,
* an empty string is added to the result.
*
* This function is used to process CSV-like strings where some elements may be enclosed in quotes
* (so that commas inside the values are not treated as separators). It also correctly handles empty fields
* between commas by inserting empty strings where necessary.
*
* @param line The input string to be split. The string may contain quoted elements, and empty fields between commas.
* @return A list of strings split by commas, taking into account quoted sections and empty fields between commas.
*
* Example:
* For the input string:
* "STOCK,BASFY,BASF SE S/ADR by BASF SE,US,OOTC,,USD,,"
* The function will return the list:
* ["STOCK", "BASFY", "BASF SE S/ADR by BASF SE", "US", "OOTC", "", "USD", ""]
*/
private fun splitWithQuotes(line: String): List<String> {
val result = mutableListOf<String>()
val regex = Pattern.compile("""("([^"]*)"|([^,]*))(,|$)""")
val matcher = regex.matcher(line)
while (matcher.find()) {
val value = matcher.group(2) ?: matcher.group(3) ?: ""
result.add(value)
}
val finalResult = mutableListOf<String>()
var currentIndex = 0
for (part in result) {
while (line.indexOf(",", currentIndex) > line.indexOf(
part,
currentIndex
) + part.length
) {
finalResult.add("")
}
finalResult.add(part)
currentIndex = line.indexOf(part, currentIndex) + part.length
}
return finalResult
}
/**
* Companion object containing constants and configuration for the DxFeedHttpIpfProvider class.
*
* @property TAG Logging tag for the class.
* @property FAIL_PAUSE_MS Time to pause in case of a failed IPF request.
*/
companion object {
private const val TAG = "DxFeedHttpIpfProvider"
private const val FAIL_PAUSE_MS = 3000L
}
}
/**
- Sealed class representing different types of errors that can occur in the DxFeedHttpIpfProvider.
*/
sealed class IpfProviderError(override val message: String, override val error: Throwable?) :
ProviderError {
data class NetworkError(
override val message: String,
override val error: Throwable? = null
) : IpfProviderError(message, error)
data class ParsingError(
override val message: String,
override val error: Throwable? = null
) : IpfProviderError(message, error)
data class HttpError(
override val message: String,
override val error: Throwable? = null
) : IpfProviderError(message, error)
data class UnknownError(
override val message: String,
override val error: Throwable? = null
) : IpfProviderError(message, error)
}